using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;

namespace TelnetExpect
{
    /// <summary>
    /// Parses ANSI commands from the peer.  Also provides conversion of special
    /// key-presses to the equivalent ANSI/vt-100 command.
    /// </summary>
    public abstract class AnsiConsoleBase
    {
        readonly Stream m_peer;

        protected AnsiConsoleBase(Stream peer)
        {
            m_peer = peer;
        }

        #region Printer
        enum State
        {
            Normal,
            SeenCtrl,
            SeenBracket,
        }

        /// <summary>
        /// Reads from the <see cref="T:System.IO.Stream"/> and parses out ANSI
        /// commands passing them to abstract method <see cref="M:OnAnsiCommand"/>
        /// and the text to <see cref="OnText"/>.
        /// </summary>
        public void RunPrinterForever()
        {
            const byte Esc = 0x1B;
            const byte Bracket = checked((byte)'[');
            State state = State.Normal;
            byte[] buf = new byte[4096];
            StringBuilder argsText = new StringBuilder();
            while (true) {
                int readLen = m_peer.Read(buf, 0, buf.Length);
                if (readLen == 0) {
                    //Arghh see when we close--
                    Console.WriteLine("[Connection closed by peer.]");
                    break;
                }
                int endPos = readLen /* + offset */;
                int nextWriteIdx = 0 /* + offset */;
                int searchPos = 0;
                while (nextWriteIdx < endPos && searchPos < endPos) {
                    if (state == State.Normal) {
                        int idx = Array.IndexOf(buf, Esc, nextWriteIdx, endPos - nextWriteIdx);
                        if (idx != -1) {
                            state = State.SeenCtrl;
                            Debug.Assert(buf[idx] == Esc, "found ctrl");
                            searchPos = idx + 1;
                            if (searchPos - 1 > nextWriteIdx) {
                                OnText(buf, nextWriteIdx, searchPos - 1 - nextWriteIdx);
                                nextWriteIdx = searchPos - 1; 
                            }
                            continue;
                        }
                    } else if (state == State.SeenCtrl) {
                        int idx = Array.IndexOf(buf, Bracket, searchPos, endPos - searchPos);
                        if (idx != -1) {
                            Debug.Assert(buf[idx] == Bracket, "found ctrl");
                            state = State.SeenBracket;
                            searchPos = idx + 1;
                            argsText.Length = 0;
                            continue;
                        } else {
                            state = State.Normal;
                            // Copy through usused Esc?
                            if (searchPos == 0) {
                                OnText(new byte[] { Esc }, 0, 1);
                            }
                        }
                    } else if (state == State.SeenBracket) {
                        bool foundEnd = false;
                        for (int idx = searchPos; !foundEnd && idx < endPos; ++idx) {
                            byte cur = buf[idx];
                            searchPos = idx + 1;
                            char curC = (char)cur;
                            if ((curC >= (char)0x40 && curC <= (char)0x7E)) {
                                state = State.Normal;
                                foundEnd = true;
                                nextWriteIdx = idx + 1;
                                AnsiCommand(curC, argsText);
                                argsText.Length = 0;
                            } else {
                                argsText.Append(curC);
                            }
                        }//for
                        continue;
                    }
                    //
                    {
                        OnText(buf, nextWriteIdx, endPos - nextWriteIdx);
                        break;
                    }
                }//while
            }//while
        }

        private void AnsiCommand(char command, StringBuilder argsText)
        {
            List<int> argList = new List<int>();
            if (argsText.Length > 0) {
                string[] argsS = argsText.ToString().Split(';');
                if (argsS.Length > 1) { //COVERAGE
                }
                foreach (string cur in argsS) {
                    int v;
                    try {
                        v = int.Parse(cur, System.Globalization.CultureInfo.InvariantCulture);
                    } catch (FormatException) {
                        Trace.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                            "Discarding AnsiCommand '{0}' = 0x{1:x4} due to unexpected arg: '{2}' = 0x{3:x4}...",
                           command, (int)command, cur, (int)cur[0]));
                        return;
                    }
                    argList.Add(v);
                }
            }
            //
            if (argsText.Length == 0) {
                OnAnsiCommand(command, new int[] { });
            } else {
                OnAnsiCommand(command, argList.ToArray());
            }
        }

        /// <summary>
        /// Called when an ANSI command is found in the <see cref="T:System.IO.Stream"/>.
        /// </summary>
        /// -
        /// <param name="command">The ANSI command code, e.g. H for 'CURSOR POSITION'.
        /// </param>
        /// <param name="vars">An array of <see cref="T:System.Int32"/> containing
        /// the parameters for the command.
        /// </param>
        /// -
        /// <remarks>?Need to handle other formats of command with different
        /// intervening text.
        /// </remarks>
        protected abstract void OnAnsiCommand(char command, int[] vars);

        /// <summary>
        /// Called when text is found in the <see cref="T:System.IO.Stream"/>.
        /// </summary>
        /// <param name="text">An array of <see cref="T:System.Byte"/>.  When this
        /// method is called it contains text bytes received from the peer.
        /// </param>
        /// <param name="offset">The zero-based byte offset in buffer at which the
        /// text bytes start.
        /// </param>
        /// <param name="count">The number of text bytes in the array.
        /// </param>
        protected abstract void OnText(byte[] text, int offset, int count);
        #endregion


        #region Keyboard
#if !NETCF
        public void WriteKeyPressTo(ConsoleKeyInfo keyInfo, bool ansi, TextWriter target)
        {
            if (target == null)
                throw new ArgumentNullException("target");
            //
            bool handled;
            if (ansi) {
                handled = WriteSpecialKeyPressTo(keyInfo, target);
            } else {
                handled = false;
            }
            if (handled) {
            } else if (keyInfo.Key == ConsoleKey.Enter) {
                target.WriteLine();
            } else if (keyInfo.KeyChar == 0) {
                Trace.WriteLine("Sent Control char needing mapping: " + keyInfo.Key);
                //wtr.Write(cki.KeyChar); // Send the null anyway!
                target.Write(keyInfo.Key.ToString()); // Send the name!!!
            } else {
                target.Write(keyInfo.KeyChar);
            }
        }

        public bool WriteSpecialKeyPressTo(ConsoleKeyInfo keyInfo, TextWriter target)
        {
            if (target == null)
                throw new ArgumentNullException("target");
            const bool ansi = true;
            const char ESC = (char)0x1B;
            char[] threeByteAnsi = { (char)0x1B, '[', /*  */ '?' };
#if ANSI_FUNCTION_KEYS // Not used by Windows apparently!!!
            char[] functionKeyAnsi = { (char)0x1B, '[', /**/ '?', (char)0x20, (char)0x57 };
#else //if
            char[] threeByteVt100 = { (char)0x1B, 'O', /*  */ '?' };
#endif
            if (ansi && keyInfo.Key == ConsoleKey.LeftArrow) {
                threeByteAnsi[2] = 'D';
                target.Write(threeByteAnsi);
            } else if (ansi && keyInfo.Key == ConsoleKey.RightArrow) {
                threeByteAnsi[2] = 'C';
                target.Write(threeByteAnsi);
            } else if (ansi && keyInfo.Key == ConsoleKey.DownArrow) {
                threeByteAnsi[2] = 'B';
                target.Write(threeByteAnsi);
            } else if (ansi && keyInfo.Key == ConsoleKey.UpArrow) {
                threeByteAnsi[2] = 'A';
                target.Write(threeByteAnsi);
#if ANSI_FUNCTION_KEYS // Not used by Windows apparently!!!
                    } else if (m_ansi && cki.Key >= ConsoleKey.F1 && cki.Key <= ConsoleKey.F24) {
                        int num = cki.Key - ConsoleKey.F1 + 1;
                        char id = checked((char)((int)'0' + num));
                        functionKeyAnsi[2] = id;
                        wtr.Write(functionKeyAnsi);
#else // if VT100_FUNCTION_KEYS
            } else if (ansi && keyInfo.Key >= ConsoleKey.F1 && keyInfo.Key <= ConsoleKey.F24) {
                Debug.Assert(ConsoleKey.F24 - ConsoleKey.F1 == 23, "contiguous");
                int num = keyInfo.Key - ConsoleKey.F1 + 1;
                if (num >= 1 && num <= 4) {
                    // ESC + 'O' + { 'P' - 'S' }
                    char id = checked((char)((int)'P' + num - 1));
                    Debug.Assert(id == 'P' || id == 'Q' || id == 'R' || id == 'S', "Illegal FnKey char: '" + id + "'");
                    threeByteVt100[2] = id;
                    target.Write(threeByteVt100);
                } else {
                    Debug.Assert(num >= 5);
                    // ESC + '[' + n + n + '~' when nn=17 to 34 etc, e.g. for F6 is "ESC[17~"
                    int num2 = num + 11;
                    if (num == 5) {
                        num2 -= 1;  // But for F5 is "ESC[15~"
                        Debug.Assert(num2 == 15);
                    }
                    if (num >= 11) {
                        num2 += 1;
                    }
                    string x = "\x001b[" + num2.ToString(System.Globalization.CultureInfo.InvariantCulture) + "~";
                    target.Write(x);
                }
            } else if (ansi && keyInfo.Key == ConsoleKey.Home) {
                string x = "\x001b[1~";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.Insert) {
                string x = "\x001b[2~";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.Delete) {
                string x = "\x001b[3~";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.End) {
                string x = "\x001b[4~";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.PageUp) {
                string x = "\x001b[5~";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.PageDown) {
                string x = "\x001b[6~";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.Tab
                    && keyInfo.Modifiers == ConsoleModifiers.Shift) {
                string x;
                x = ESC + "[z"; // ANSI
                x = ESC + "[0Z"; // VT 220
                x = ESC + "[Z"; // VT 100
                target.Write(x);
            } else if (ansi && keyInfo.Key >= ConsoleKey.NumPad0
                    && keyInfo.Key <= ConsoleKey.NumPad9) {
                Debug.Assert(ConsoleKey.NumPad9 - ConsoleKey.NumPad0 == 9, "contiguous");
                int num = keyInfo.Key - ConsoleKey.NumPad0;
                string x = "\x001bO" + (char)((int)'p' + num);
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.Multiply) {
                string x = "\x001bOj";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.Add) {
                string x = "\x001bOk";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.Subtract) {
                string x = "\x001bOm";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.Decimal) {
                string x = "\x001bOn";
                target.Write(x);
            } else if (ansi && keyInfo.Key == ConsoleKey.Divide) {
                string x = "\x001bOo";
                target.Write(x);
#endif
            } else {
                return false;
            }
            return true;
        }
#endif

        public bool WinFormsWriteKeyPressTo(System.Windows.Forms.KeyEventArgs keyInfo, bool ansi, TextWriter target)
        {
            if (target == null)
                throw new ArgumentNullException("target");
            //
            bool handled;
            if (ansi) {
                handled = WinFormsWriteSpecialKeyPressTo(keyInfo, target);
            } else {
                handled = false;
            }
            if (handled) {
                Trace.WriteLine("Sent a control key.");
            } else if (keyInfo.KeyCode == System.Windows.Forms.Keys.Enter) {
                Trace.WriteLine("Sending WriteLine");
                target.WriteLine();
                handled = true;
            //TODO WinForms } else if (keyInfo.KeyChar == 0) {
            //    Trace.WriteLine("Sent Control char needing mapping: " + keyInfo.KeyCode);
            //    //wtr.Write(cki.KeyChar); // Send the null anyway!
            //    target.Write(keyInfo.KeyCode.ToString()); // Send the name!!!
            //} else {
            //    target.Write(keyInfo.KeyChar);
            }
            return handled;
        }

        public bool WinFormsWriteSpecialKeyPressTo(System.Windows.Forms.KeyEventArgs keyInfo, TextWriter target)
        {
            if (target == null)
                throw new ArgumentNullException("target");
            const bool ansi = true;
            const char ESC = (char)0x1B;
            char[] threeByteAnsi = { (char)0x1B, '[', /*  */ '?' };
#if ANSI_FUNCTION_KEYS // Not used by Windows apparently!!!
            char[] functionKeyAnsi = { (char)0x1B, '[', /**/ '?', (char)0x20, (char)0x57 };
#else //if
            char[] threeByteVt100 = { (char)0x1B, 'O', /*  */ '?' };
#endif
            if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Left) {
                threeByteAnsi[2] = 'D';
                target.Write(threeByteAnsi);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Right) {
                threeByteAnsi[2] = 'C';
                target.Write(threeByteAnsi);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Down) {
                threeByteAnsi[2] = 'B';
                target.Write(threeByteAnsi);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Up) {
                threeByteAnsi[2] = 'A';
                target.Write(threeByteAnsi);
#if ANSI_FUNCTION_KEYS // Not used by Windows apparently!!!
                    } else if (m_ansi && cki.Key >= System.Windows.Forms.Keys.F1 && cki.Key <= System.Windows.Forms.Keys.F24) {
                        int num = cki.Key - System.Windows.Forms.Keys.F1 + 1;
                        char id = checked((char)((int)'0' + num));
                        functionKeyAnsi[2] = id;
                        wtr.Write(functionKeyAnsi);
#else // if VT100_FUNCTION_KEYS
            } else if (ansi && keyInfo.KeyCode >= System.Windows.Forms.Keys.F1 && keyInfo.KeyCode <= System.Windows.Forms.Keys.F24) {
                Debug.Assert(System.Windows.Forms.Keys.F24 - System.Windows.Forms.Keys.F1 == 23, "contiguous");
                int num = keyInfo.KeyCode - System.Windows.Forms.Keys.F1 + 1;
                if (num >= 1 && num <= 4) {
                    // ESC + 'O' + { 'P' - 'S' }
                    char id = checked((char)((int)'P' + num - 1));
                    Debug.Assert(id == 'P' || id == 'Q' || id == 'R' || id == 'S', "Illegal FnKey char: '" + id + "'");
                    threeByteVt100[2] = id;
                    target.Write(threeByteVt100);
                } else {
                    Debug.Assert(num >= 5);
                    // ESC + '[' + n + n + '~' when nn=17 to 34 etc, e.g. for F6 is "ESC[17~"
                    int num2 = num + 11;
                    if (num == 5) {
                        num2 -= 1;  // But for F5 is "ESC[15~"
                        Debug.Assert(num2 == 15);
                    }
                    if (num >= 11) {
                        num2 += 1;
                    }
                    string x = "\x001b[" + num2.ToString(System.Globalization.CultureInfo.InvariantCulture) + "~";
                    target.Write(x);
                }
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Home) {
                string x = "\x001b[1~";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Insert) {
                string x = "\x001b[2~";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Delete) {
                string x = "\x001b[3~";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.End) {
                string x = "\x001b[4~";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.PageUp) {
                string x = "\x001b[5~";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.PageDown) {
                string x = "\x001b[6~";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Tab
                    && keyInfo.Modifiers == System.Windows.Forms.Keys.Shift) {
                string x;
                x = ESC + "[z"; // ANSI
                x = ESC + "[0Z"; // VT 220
                x = ESC + "[Z"; // VT 100
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode >= System.Windows.Forms.Keys.NumPad0
                    && keyInfo.KeyCode <= System.Windows.Forms.Keys.NumPad9) {
                Debug.Assert(System.Windows.Forms.Keys.NumPad9 - System.Windows.Forms.Keys.NumPad0 == 9, "contiguous");
                int num = keyInfo.KeyCode - System.Windows.Forms.Keys.NumPad0;
                string x = "\x001bO" + (char)((int)'p' + num);
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Multiply) {
                string x = "\x001bOj";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Add) {
                string x = "\x001bOk";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Subtract) {
                string x = "\x001bOm";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Decimal) {
                string x = "\x001bOn";
                target.Write(x);
            } else if (ansi && keyInfo.KeyCode == System.Windows.Forms.Keys.Divide) {
                string x = "\x001bOo";
                target.Write(x);
#endif
            } else {
                return false;
            }
            return true;
        }
        #endregion

    }
}
